// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
    /// <summary>
    /// <see cref="IModelBinder"/> implementation for binding complex types.
    /// </summary>
    public sealed class ComplexObjectModelBinder : IModelBinder
    {
        // Don't want a new public enum because communication between the private and internal methods of this class
        // should not be exposed. Can't use an internal enum because types of [TheoryData] values must be public.

        // Model contains only properties that are expected to bind from value providers and no value provider has
        // matching data.
        internal const int NoDataAvailable = 0;
        // If model contains properties that are expected to bind from value providers, no value provider has matching
        // data. Remaining (greedy) properties might bind successfully.
        internal const int GreedyPropertiesMayHaveData = 1;
        // Model contains at least one property that is expected to bind from value providers and a value provider has
        // matching data.
        internal const int ValueProviderDataAvailable = 2;

        private readonly IDictionary<ModelMetadata, IModelBinder> _propertyBinders;
        private readonly IReadOnlyList<IModelBinder> _parameterBinders;
        private readonly ILogger _logger;
        private Func<object>? _modelCreator;

        internal ComplexObjectModelBinder(
            IDictionary<ModelMetadata, IModelBinder> propertyBinders,
            IReadOnlyList<IModelBinder> parameterBinders,
            ILogger<ComplexObjectModelBinder> logger)
        {
            _propertyBinders = propertyBinders;
            _parameterBinders = parameterBinders;
            _logger = logger;
        }

        /// <inheritdoc/>
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            _logger.AttemptingToBindModel(bindingContext);

            var parameterData = CanCreateModel(bindingContext);
            if (parameterData == NoDataAvailable)
            {
                return Task.CompletedTask;
            }

            // Perf: separated to avoid allocating a state machine when we don't
            // need to go async.
            return BindModelCoreAsync(bindingContext, parameterData);
        }

        private async Task BindModelCoreAsync(ModelBindingContext bindingContext, int propertyData)
        {
            Debug.Assert(propertyData == GreedyPropertiesMayHaveData || propertyData == ValueProviderDataAvailable);

            // Create model first (if necessary) to avoid reporting errors about properties when activation fails.
            var attemptedBinding = false;
            var bindingSucceeded = false;

            var modelMetadata = bindingContext.ModelMetadata;
            var boundConstructor = modelMetadata.BoundConstructor;

            if (boundConstructor != null)
            {
                // Only record types are allowed to have a BoundConstructor. Binding a record type requires
                // instantiating the type. This means we'll ignore a previously assigned bindingContext.Model value.
                // This behaior is identical to input formatting with S.T.Json and Json.NET.
 
                var values = new object[boundConstructor.BoundConstructorParameters!.Count];
                var (attemptedParameterBinding, parameterBindingSucceeded) = await BindParametersAsync(
                    bindingContext,
                    propertyData,
                    boundConstructor.BoundConstructorParameters,
                    values);

                attemptedBinding |= attemptedParameterBinding;
                bindingSucceeded |= parameterBindingSucceeded;

                if (!CreateModel(bindingContext, boundConstructor, values))
                {
                    return;
                }
            }
            else if (bindingContext.Model == null)
            {
                CreateModel(bindingContext);
            }

            var (attemptedPropertyBinding, propertyBindingSucceeded) = await BindPropertiesAsync(
                bindingContext,
                propertyData,
                modelMetadata.BoundProperties);

            attemptedBinding |= attemptedPropertyBinding;
            bindingSucceeded |= propertyBindingSucceeded;

            // Have we created a top-level model despite an inability to bind anything in said model and a lack of
            // other IsBindingRequired errors? Does that violate [BindRequired] on the model? This case occurs when
            // 1. The top-level model has no public settable properties.
            // 2. All properties in a [BindRequired] model have [BindNever] or are otherwise excluded from binding.
            // 3. No data exists for any property.
            if (!attemptedBinding &&
                bindingContext.IsTopLevelObject &&
                modelMetadata.IsBindingRequired)
            {
                var messageProvider = modelMetadata.ModelBindingMessageProvider;
                var message = messageProvider.MissingBindRequiredValueAccessor(bindingContext.FieldName);
                bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
            }

            _logger.DoneAttemptingToBindModel(bindingContext);

            // Have all binders failed because no data was available?
            //
            // If CanCreateModel determined a property has data, failures are likely due to conversion errors. For
            // example, user may submit ?[0].id=twenty&[1].id=twenty-one&[2].id=22 for a collection of a complex type
            // with an int id property. In that case, the bound model should be [ {}, {}, { id = 22 }] and
            // ModelState should contain errors about both [0].id and [1].id. Do not inform higher-level binders of the
            // failure in this and similar cases.
            //
            // If CanCreateModel could not find data for non-greedy properties, failures indicate greedy binders were
            // unsuccessful. For example, user may submit file attachments [0].File and [1].File but not [2].File for
            // a collection of a complex type containing an IFormFile property. In that case, we have exhausted the
            // attached files and checking for [3].File is likely be pointless. (And, if it had a point, would we stop
            // after 10 failures, 100, or more -- all adding redundant errors to ModelState?) Inform higher-level
            // binders of the failure.
            //
            // Required properties do not change the logic below. Missed required properties cause ModelState errors
            // but do not necessarily prevent further attempts to bind.
            //
            // This logic is intended to maximize correctness but does not avoid infinite loops or recursion when a
            // greedy model binder succeeds unconditionally.
            if (!bindingContext.IsTopLevelObject &&
                !bindingSucceeded &&
                propertyData == GreedyPropertiesMayHaveData)
            {
                bindingContext.Result = ModelBindingResult.Failed();
                return;
            }

            bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
        }

        internal static bool CreateModel(ModelBindingContext bindingContext, ModelMetadata boundConstructor, object[] values)
        {
            try
            {
                bindingContext.Model = boundConstructor.BoundConstructorInvoker!(values);
                return true;
            }
            catch (Exception ex)
            {
                AddModelError(ex, bindingContext.ModelName, bindingContext);
                bindingContext.Result = ModelBindingResult.Failed();
                return false;
            }
        }

        /// <summary>
        /// Creates suitable <see cref="object"/> for given <paramref name="bindingContext"/>.
        /// </summary>
        /// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
        /// <returns>An <see cref="object"/> compatible with <see cref="ModelBindingContext.ModelType"/>.</returns>
        internal void CreateModel(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            // If model creator throws an exception, we want to propagate it back up the call stack, since the
            // application developer should know that this was an invalid type to try to bind to.
            if (_modelCreator == null)
            {
                // The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as
                // reflection does not provide information about the implicit parameterless constructor for a struct.
                // This binder would eventually fail to construct an instance of the struct as the Linq's NewExpression
                // compile fails to construct it.
                var modelType = bindingContext.ModelType;
                if (modelType.IsAbstract || modelType.GetConstructor(Type.EmptyTypes) == null)
                {
                    var metadata = bindingContext.ModelMetadata;
                    switch (metadata.MetadataKind)
                    {
                        case ModelMetadataKind.Parameter:
                            throw new InvalidOperationException(
                                Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForParameter(
                                    modelType.FullName,
                                    metadata.ParameterName));
                        case ModelMetadataKind.Property:
                            throw new InvalidOperationException(
                                Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForProperty(
                                    modelType.FullName,
                                    metadata.PropertyName,
                                    bindingContext.ModelMetadata.ContainerType!.FullName));
                        case ModelMetadataKind.Type:
                            throw new InvalidOperationException(
                                Resources.FormatComplexObjectModelBinder_NoSuitableConstructor_ForType(
                                    modelType.FullName));
                    }
                }

                _modelCreator = Expression
                    .Lambda<Func<object>>(Expression.New(bindingContext.ModelType))
                    .Compile();
            }

            bindingContext.Model = _modelCreator();
        }

        private async ValueTask<(bool attemptedBinding, bool bindingSucceeded)> BindParametersAsync(
            ModelBindingContext bindingContext,
            int propertyData,
            IReadOnlyList<ModelMetadata> parameters,
            object?[] parameterValues)
        {
            var attemptedBinding = false;
            var bindingSucceeded = false;

            if (parameters.Count == 0)
            {
                return (attemptedBinding, bindingSucceeded);
            }

            var postponePlaceholderBinding = false;
            for (var i = 0; i < parameters.Count; i++)
            {
                var parameter = parameters[i];
                
                var fieldName = parameter.BinderModelName ?? parameter.ParameterName!;
                var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);

                if (!CanBindItem(bindingContext, parameter))
                {
                    continue;
                }

                var parameterBinder = _parameterBinders[i];
                if (parameterBinder is PlaceholderBinder)
                {
                    if (postponePlaceholderBinding)
                    {
                        // Decided to postpone binding properties that complete a loop in the model types when handling
                        // an earlier loop-completing property. Postpone binding this property too.
                        continue;
                    }
                    else if (!bindingContext.IsTopLevelObject &&
                        !bindingSucceeded &&
                        propertyData == GreedyPropertiesMayHaveData)
                    {
                        // Have no confirmation of data for the current instance. Postpone completing the loop until
                        // we _know_ the current instance is useful. Recursion would otherwise occur prior to the
                        // block with a similar condition after the loop.
                        //
                        // Example cases include an Employee class containing
                        // 1. a Manager property of type Employee
                        // 2. an Employees property of type IList<Employee>
                        postponePlaceholderBinding = true;
                        continue;
                    }
                }

                var result = await BindParameterAsync(bindingContext, parameter, parameterBinder, fieldName, modelName);

                if (result.IsModelSet)
                {
                    attemptedBinding = true;
                    bindingSucceeded = true;

                    parameterValues[i] = result.Model;
                }
                else if (parameter.IsBindingRequired)
                {
                    attemptedBinding = true;
                }
            }

            if (postponePlaceholderBinding && bindingSucceeded)
            {
                // Have some data for this instance. Continue with the model type loop.
                for (var i = 0; i < parameters.Count; i++)
                {
                    var parameter = parameters[i];
                    if (!CanBindItem(bindingContext, parameter))
                    {
                        continue;
                    }

                    var parameterBinder = _parameterBinders[i];
                    if (parameterBinder is PlaceholderBinder)
                    {
                        var fieldName = parameter.BinderModelName ?? parameter.ParameterName!;
                        var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);

                        var result = await BindParameterAsync(bindingContext, parameter, parameterBinder, fieldName, modelName);

                        if (result.IsModelSet)
                        {
                            parameterValues[i] = result.Model;
                        }
                    }
                }
            }

            return (attemptedBinding, bindingSucceeded);
        }

        private async ValueTask<(bool attemptedBinding, bool bindingSucceeded)> BindPropertiesAsync(
            ModelBindingContext bindingContext,
            int propertyData,
            IReadOnlyList<ModelMetadata> boundProperties)
        {
            var attemptedBinding = false;
            var bindingSucceeded = false;

            if (boundProperties.Count == 0)
            {
                return (attemptedBinding, bindingSucceeded);
            }

            var postponePlaceholderBinding = false;
            for (var i = 0; i < boundProperties.Count; i++)
            {
                var property = boundProperties[i];
                if (!CanBindItem(bindingContext, property))
                {
                    continue;
                }

                var propertyBinder = _propertyBinders[property];
                if (propertyBinder is PlaceholderBinder)
                {
                    if (postponePlaceholderBinding)
                    {
                        // Decided to postpone binding properties that complete a loop in the model types when handling
                        // an earlier loop-completing property. Postpone binding this property too.
                        continue;
                    }
                    else if (!bindingContext.IsTopLevelObject &&
                        !bindingSucceeded &&
                        propertyData == GreedyPropertiesMayHaveData)
                    {
                        // Have no confirmation of data for the current instance. Postpone completing the loop until
                        // we _know_ the current instance is useful. Recursion would otherwise occur prior to the
                        // block with a similar condition after the loop.
                        //
                        // Example cases include an Employee class containing
                        // 1. a Manager property of type Employee
                        // 2. an Employees property of type IList<Employee>
                        postponePlaceholderBinding = true;
                        continue;
                    }
                }

                var fieldName = property.BinderModelName ?? property.PropertyName!;
                var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
                var result = await BindPropertyAsync(bindingContext, property, propertyBinder, fieldName, modelName);

                if (result.IsModelSet)
                {
                    attemptedBinding = true;
                    bindingSucceeded = true;
                }
                else if (property.IsBindingRequired)
                {
                    attemptedBinding = true;
                }
            }

            if (postponePlaceholderBinding && bindingSucceeded)
            {
                // Have some data for this instance. Continue with the model type loop.
                for (var i = 0; i < boundProperties.Count; i++)
                {
                    var property = boundProperties[i];
                    if (!CanBindItem(bindingContext, property))
                    {
                        continue;
                    }

                    var propertyBinder = _propertyBinders[property];
                    if (propertyBinder is PlaceholderBinder)
                    {
                        var fieldName = property.BinderModelName ?? property.PropertyName!;
                        var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);

                        await BindPropertyAsync(bindingContext, property, propertyBinder, fieldName, modelName);
                    }
                }
            }

            return (attemptedBinding, bindingSucceeded);
        }

        internal bool CanBindItem(ModelBindingContext bindingContext, ModelMetadata propertyMetadata)
        {
            var metadataProviderFilter = bindingContext.ModelMetadata.PropertyFilterProvider?.PropertyFilter;
            if (metadataProviderFilter?.Invoke(propertyMetadata) == false)
            {
                return false;
            }

            if (bindingContext.PropertyFilter?.Invoke(propertyMetadata) == false)
            {
                return false;
            }

            if (!propertyMetadata.IsBindingAllowed)
            {
                return false;
            }

            if (propertyMetadata.MetadataKind == ModelMetadataKind.Property && propertyMetadata.IsReadOnly)
            {
                // Determine if we can update a readonly property (such as a collection).
                return CanUpdateReadOnlyProperty(propertyMetadata.ModelType);
            }

            return true;
        }

        private async ValueTask<ModelBindingResult> BindPropertyAsync(
            ModelBindingContext bindingContext,
            ModelMetadata property,
            IModelBinder propertyBinder,
            string fieldName,
            string modelName)
        {
            Debug.Assert(property.MetadataKind == ModelMetadataKind.Property);

            // Pass complex (including collection) values down so that binding system does not unnecessarily
            // recreate instances or overwrite inner properties that are not bound. No need for this with simple
            // values because they will be overwritten if binding succeeds. Arrays are never reused because they
            // cannot be resized.
            object? propertyModel = null;
            if (property.PropertyGetter != null &&
                property.IsComplexType &&
                !property.ModelType.IsArray)
            {
                propertyModel = property.PropertyGetter(bindingContext.Model!);
            }

            ModelBindingResult result;
            using (bindingContext.EnterNestedScope(
                modelMetadata: property,
                fieldName: fieldName,
                modelName: modelName,
                model: propertyModel))
            {
                await propertyBinder.BindModelAsync(bindingContext);
                result = bindingContext.Result;
            }

            if (result.IsModelSet)
            {
                SetProperty(bindingContext, modelName, property, result);
            }
            else if (property.IsBindingRequired)
            {
                var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
                bindingContext.ModelState.TryAddModelError(modelName, message);
            }

            return result;
        }

        private async ValueTask<ModelBindingResult> BindParameterAsync(
            ModelBindingContext bindingContext,
            ModelMetadata parameter,
            IModelBinder parameterBinder,
            string fieldName,
            string modelName)
        {
            Debug.Assert(parameter.MetadataKind == ModelMetadataKind.Parameter);

            ModelBindingResult result;
            using (bindingContext.EnterNestedScope(
                modelMetadata: parameter,
                fieldName: fieldName,
                modelName: modelName,
                model: null))
            {
                await parameterBinder.BindModelAsync(bindingContext);
                result = bindingContext.Result;
            }

            if (!result.IsModelSet && parameter.IsBindingRequired)
            {
                var message = parameter.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
                bindingContext.ModelState.TryAddModelError(modelName, message);
            }

            return result;
        }

        internal int CanCreateModel(ModelBindingContext bindingContext)
        {
            var isTopLevelObject = bindingContext.IsTopLevelObject;

            // If we get here the model is a complex object which was not directly bound by any previous model binder,
            // so we want to decide if we want to continue binding. This is important to get right to avoid infinite
            // recursion.
            //
            // First, we want to make sure this object is allowed to come from a value provider source as this binder
            // will only include value provider data. For instance if the model is marked with [FromBody], then we
            // can just skip it. A greedy source cannot be a value provider.
            //
            // If the model isn't marked with ANY binding source, then we assume it's OK also.
            //
            // We skip this check if it is a top level object because we want to always evaluate
            // the creation of top level object (this is also required for ModelBinderAttribute to work.)
            var bindingSource = bindingContext.BindingSource;
            if (!isTopLevelObject && bindingSource != null && bindingSource.IsGreedy)
            {
                return NoDataAvailable;
            }

            // Create the object if:
            // 1. It is a top level model.
            if (isTopLevelObject)
            {
                return ValueProviderDataAvailable;
            }

            // 2. Any of the model properties can be bound.
            return CanBindAnyModelItem(bindingContext);
        }

        private int CanBindAnyModelItem(ModelBindingContext bindingContext)
        {
            // If there are no properties on the model, and no constructor parameters, there is nothing to bind. We are here means this is not a top
            // level object. So we return false.
            var modelMetadata = bindingContext.ModelMetadata;
            var performsConstructorBinding = bindingContext.Model == null && modelMetadata.BoundConstructor != null;

            if (modelMetadata.Properties.Count == 0 &&
                 (!performsConstructorBinding || modelMetadata.BoundConstructor!.BoundConstructorParameters!.Count == 0))
            {
                Log.NoPublicSettableItems(_logger, bindingContext);
                return NoDataAvailable;
            }

            // We want to check to see if any of the properties of the model can be bound using the value providers or
            // a greedy binder.
            //
            // Because a property might specify a custom binding source ([FromForm]), it's not correct
            // for us to just try bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName);
            // that may include other value providers - that would lead us to mistakenly create the model
            // when the data is coming from a source we should use (ex: value found in query string, but the
            // model has [FromForm]).
            //
            // To do this we need to enumerate the properties, and see which of them provide a binding source
            // through metadata, then we decide what to do.
            //
            //      If a property has a binding source, and it's a greedy source, then it's always bound.
            //
            //      If a property has a binding source, and it's a non-greedy source, then we'll filter the
            //      the value providers to just that source, and see if we can find a matching prefix
            //      (see CanBindValue).
            //
            //      If a property does not have a binding source, then it's fair game for any value provider.
            //
            // Bottom line, if any property meets the above conditions and has a value from ValueProviders, then we'll
            // create the model and try to bind it. Of, if ANY properties of the model have a greedy source,
            // then we go ahead and create it.
            var hasGreedyBinders = false;
            for (var i = 0; i < bindingContext.ModelMetadata.Properties.Count; i++)
            {
                var propertyMetadata = bindingContext.ModelMetadata.Properties[i];
                if (!CanBindItem(bindingContext, propertyMetadata))
                {
                    continue;
                }

                // If any property can be bound from a greedy binding source, then success.
                var bindingSource = propertyMetadata.BindingSource;
                if (bindingSource != null && bindingSource.IsGreedy)
                {
                    hasGreedyBinders = true;
                    continue;
                }

                // Otherwise, check whether the (perhaps filtered) value providers have a match.
                var fieldName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName!;
                var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
                using (bindingContext.EnterNestedScope(
                    modelMetadata: propertyMetadata,
                    fieldName: fieldName,
                    modelName: modelName,
                    model: null))
                {
                    // If any property can be bound from a value provider, then success.
                    if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
                    {
                        return ValueProviderDataAvailable;
                    }
                }
            }

            if (performsConstructorBinding)
            {
                var parameters = bindingContext.ModelMetadata.BoundConstructor!.BoundConstructorParameters!;
                for (var i = 0; i < parameters.Count; i++)
                {
                    var parameterMetadata = parameters[i];
                    if (!CanBindItem(bindingContext, parameterMetadata))
                    {
                        continue;
                    }

                    // If any parameter can be bound from a greedy binding source, then success.
                    var bindingSource = parameterMetadata.BindingSource;
                    if (bindingSource != null && bindingSource.IsGreedy)
                    {
                        hasGreedyBinders = true;
                        continue;
                    }

                    // Otherwise, check whether the (perhaps filtered) value providers have a match.
                    var fieldName = parameterMetadata.BinderModelName ?? parameterMetadata.ParameterName!;
                    var modelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, fieldName);
                    using (bindingContext.EnterNestedScope(
                        modelMetadata: parameterMetadata,
                        fieldName: fieldName,
                        modelName: modelName,
                        model: null))
                    {
                        // If any parameter can be bound from a value provider, then success.
                        if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
                        {
                            return ValueProviderDataAvailable;
                        }
                    }
                }
            }

            if (hasGreedyBinders)
            {
                return GreedyPropertiesMayHaveData;
            }

            _logger.CannotBindToComplexType(bindingContext);

            return NoDataAvailable;
        }

        internal static bool CanUpdateReadOnlyProperty(Type propertyType)
        {
            // Value types have copy-by-value semantics, which prevents us from updating
            // properties that are marked readonly.
            if (propertyType.IsValueType)
            {
                return false;
            }

            // Arrays are strange beasts since their contents are mutable but their sizes aren't.
            // Therefore we shouldn't even try to update these. Further reading:
            // http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx
            if (propertyType.IsArray)
            {
                return false;
            }

            // Special-case known immutable reference types
            if (propertyType == typeof(string))
            {
                return false;
            }

            return true;
        }

        internal void SetProperty(
            ModelBindingContext bindingContext,
            string modelName,
            ModelMetadata propertyMetadata,
            ModelBindingResult result)
        {
            if (!result.IsModelSet)
            {
                // If we don't have a value, don't set it on the model and trounce a pre-initialized value.
                return;
            }

            if (propertyMetadata.IsReadOnly)
            {
                // The property should have already been set when we called BindPropertyAsync, so there's
                // nothing to do here.
                return;
            }

            var value = result.Model;
            try
            {
                propertyMetadata.PropertySetter!(bindingContext.Model!, value);
            }
            catch (Exception exception)
            {
                AddModelError(exception, modelName, bindingContext);
            }
        }

        private static void AddModelError(
            Exception exception,
            string modelName,
            ModelBindingContext bindingContext)
        {
            var targetInvocationException = exception as TargetInvocationException;
            if (targetInvocationException?.InnerException != null)
            {
                exception = targetInvocationException.InnerException;
            }

            // Do not add an error message if a binding error has already occurred for this property.
            var modelState = bindingContext.ModelState;
            var validationState = modelState.GetFieldValidationState(modelName);
            if (validationState == ModelValidationState.Unvalidated)
            {
                modelState.AddModelError(modelName, exception, bindingContext.ModelMetadata);
            }
        }

        private static class Log
        {
            private static readonly Action<ILogger, string, Type, Exception?> _noPublicSettableProperties = LoggerMessage.Define<string, Type>(
               LogLevel.Debug,
                new EventId(17, "NoPublicSettableItems"),
               "Could not bind to model with name '{ModelName}' and type '{ModelType}' as the type has no public settable properties or constructor parameters.");

            public static void NoPublicSettableItems(ILogger logger, ModelBindingContext bindingContext)
            {
                _noPublicSettableProperties(logger, bindingContext.ModelName, bindingContext.ModelType, null);
            }
        }
    }
}
